/******************************************************************************
	Cobalt - utility libraries for building XNA games
	Copyright (C) 2008 Philip Djonov

	This program is free software; you can redistribute it and/or modify it
	under the terms of the GNU General Public License as published by the Free
	Software Foundation; either version 2 of the License, or (at your option)
	any later version.

	This program is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
	or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
	for more details.

	You should have received a copy of the GNU General Public License along
	with this program; if not, write to the
	
		Free Software Foundation, Inc.
		51 Franklin Street, Fifth Floor
		Boston, MA  02110-1301, USA.
******************************************************************************/

using System;
using System.Collections;
using System.IO;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Graphics;

namespace Cobalt.Content.Pipeline.Quake.Shaders
{

	public class ShaderParser
	{
		private ContentBuildLogger logger;
		public ContentBuildLogger Logger { get { return logger; } set { logger = value; } }

		public Shader ParseShader( ContentTokenReader reader )
		{
			string shaderName = reader.ReadRequiredToken( TokenReadFlags.RequireLineStart );
			reader.ReadExactToken( "{", TokenReadFlags.RequireLineStart );
			return ParseShaderContents( shaderName, reader );
		}

		public Shader ParseShaderContents( string shaderName, ContentTokenReader reader )
		{
			Shader ret = new Shader( shaderName );
			ret.IsExplicitelyDefined = true;

			for( ; ; )
			{
				string tok = reader.ReadToken( TokenReadFlags.RequireLineStart );
				if( tok == null || tok == "}" )
					break;

				if( tok.StartsWith( "qer", StringComparison.OrdinalIgnoreCase ) ||
					tok.StartsWith( "q3map", StringComparison.OrdinalIgnoreCase ) )
				{
					reader.ReadToEndOfLine();
					continue;
				}

				try
				{
					switch( tok.ToLowerInvariant() )
					{
					case "{":
						ret.Stages.Add( ParseShaderStage( reader ) );
						break;

					case "deformvertexes":
						ret.Deformations.Add( ParseVertexDeformation( reader ) );
						break;

					case "polygonoffset":
						tok = reader.ReadToken( TokenReadFlags.NoLineBreak );
						ret.PolygonOffset = (tok != null) ? int.Parse( tok ) : 1;
						break;

					case "skyparms":
						ret.Sky = ParseSky( reader );
						break;

					case "cull":
						switch( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ).ToLowerInvariant() )
						{
						case "none":
						case "twosided":
						case "disable":
							ret.Cull = CullMode.None;
							break;

						case "back":
						case "backside":
						case "backsided":
							ret.Cull = CullMode.CullClockwiseFace;
							break;

						default:
							HandleInvalidParam( reader, "cull" );
							break;
						}
						break;

					case "sort":
						ret.Sort = ParseSort( reader );
						break;

					case "portal":
						ret.Sort = ShaderSort.Portal;
						break;

					default:
						reader.ReadToEndOfLine();
						break;
					}
				}
				catch( Exception ex )
				{
					throw new InvalidContentException( "Error parsing shader.", reader.LastTokenIdentity, ex );
				}
			}

			return ret;
		}

		private ShaderSort ParseSort( ContentTokenReader reader )
		{
			switch( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ).ToLowerInvariant() )
			{
			case "portal":
				return ShaderSort.Portal;
			case "sky":
				return ShaderSort.Environment;
			case "opaque":
				return ShaderSort.Opaque;
			case "decal":
				return ShaderSort.Decal;
			case "seeThrough":
				return ShaderSort.SeeThrough;
			case "banner":
				return ShaderSort.Banner;
			case "glass":
				return ShaderSort.Blend0;
			case "additive":
				return ShaderSort.Blend1;
			case "nearest":
				return ShaderSort.Nearest;
			case "underwater":
				return ShaderSort.Underwater;
			default:
				return (ShaderSort)int.Parse( reader.LastToken );
			}
		}

		private VertexDeformation ParseVertexDeformation( ContentTokenReader reader )
		{
			try
			{
				switch( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ).ToLowerInvariant() )
				{
				case "bulge":
					BulgeDeformation bulge = new BulgeDeformation();
					bulge.Width = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
					bulge.Height = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
					bulge.Speed = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
					return bulge;

				case "wave":
					WaveDeformation wave = new WaveDeformation();
					float waveSpread = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
					wave.Spread = waveSpread != 0 ? 1.0F / waveSpread : 100.0F;
					wave.Waveform = ParseWaveform( reader );
					return wave;

				case "normal":
					NormalDeformation normal = new NormalDeformation();
					normal.Amplitude = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
					normal.Frequency = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
					return normal;

				case "expand":
					ExpandDeformation expand = new ExpandDeformation();
					expand.Amplitude = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
					return expand;

				default:
					HandleInvalidParam( reader, "vertexdeformation" );
					return null; //stfu compiler warning!
				}
			}
			catch( Exception ex )
			{
				throw new InvalidContentException( "Error parsing vertex deformation.", reader.LastTokenIdentity, ex );
			}
		}

		private string ParseImageName( string imageName )
		{
			switch( imageName.ToLowerInvariant() )
			{
			case "$whiteimage":
				return TextureBundle.WhiteImage;

			case "$lightmap":
				return TextureBundle.Lightmap;

			default:
				return imageName;
			}
		}

		private ShaderStage ParseShaderStage( ContentTokenReader reader )
		{
			try
			{
				ShaderStage ret = new ShaderStage();
				TextureBundle bundle = StartBundle();

				bool explicitDepth = false;
				
				for( ; ; )
				{
					string tok = reader.ReadRequiredToken( TokenReadFlags.RequireLineStart );
					if( tok == "}" )
						break;

					switch( tok.ToLowerInvariant() )
					{
					case "map":
						if( bundle.Images.Count != 0 )
						{
							//multi-bundle stage (not standard quake)
							EndBundle( bundle );
							ret.Bundles.Add( bundle );
							bundle = StartBundle();
						}

						string imageName = reader.ReadRequiredToken( TokenReadFlags.NoLineBreak );					
						bundle.Images.Add( ParseImageName( imageName ) );
						break;
						
					case "clampmap":
						if( bundle.Images.Count != 0 )
						{
							//multi-bundle stage (not standard quake)
							EndBundle( bundle );
							ret.Bundles.Add( bundle );
							bundle = StartBundle();
						}

						bundle.AddressMode = TextureAddressMode.Clamp;
						goto case "map";

					case "animmap":
						if( bundle.Images.Count != 0 )
						{
							//multi-bundle stage (not standard quake)
							EndBundle( bundle );
							ret.Bundles.Add( bundle );
							bundle = StartBundle();
						}

						bundle.ImageAnimationSpeed = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );

						for( ; ; )
						{
							string animImageName = reader.ReadToken( TokenReadFlags.NoLineBreak );
							if( animImageName == null )
								break;

							bundle.Images.Add( ParseImageName( animImageName ) );
						}
						break;

					case "alphafunc":
						ret.AlphaTest.Enable = true;

						switch( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ).ToLowerInvariant() )
						{
						case "gt0":
							ret.AlphaTest.Function = CompareFunction.Greater;
							ret.AlphaTest.Reference = 0;
							break;

						case "lt128":
							ret.AlphaTest.Function = CompareFunction.Less;
							ret.AlphaTest.Reference = 128;
							break;

						case "ge128":
							ret.AlphaTest.Function = CompareFunction.GreaterEqual;
							ret.AlphaTest.Reference = 128;
							break;

						default:
							HandleInvalidParam( reader, "alphafunc" );
							break;
						}
						break;

					case "depthfunc":
						switch( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ).ToLowerInvariant() )
						{
						case "lequal":
							ret.DepthTest.Function = CompareFunction.LessEqual;
							break;

						case "equal":
							ret.DepthTest.Function = CompareFunction.Equal;
							break;

						default:
							HandleInvalidParam( reader, "depthfunc" );
							break;
						}
						break; 

					case "detail":
						break;

					case "blendfunc":
						if( !explicitDepth )
							ret.DepthTest.WriteEnable = false;

						string srcBlend = reader.ReadRequiredToken( TokenReadFlags.NoLineBreak );
						switch( srcBlend.ToLowerInvariant() )
						{
						case "add":
							ret.AlphaBlend.SourceBlend = Blend.One;
							ret.AlphaBlend.DestBlend = Blend.One;
							break;

						case "blend":
							ret.AlphaBlend.SourceBlend = Blend.SourceAlpha;
							ret.AlphaBlend.DestBlend = Blend.InverseSourceAlpha;
							break;

						case "filter":
							ret.AlphaBlend.SourceBlend = Blend.DestinationColor;
							ret.AlphaBlend.DestBlend = Blend.Zero;
							break;

						default:
							ret.AlphaBlend.SourceBlend = ParseBlend( srcBlend );
							ret.AlphaBlend.DestBlend = ParseBlend( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							break;
						}

						ret.AlphaBlend.Enable =
							ret.AlphaBlend.SourceBlend != Blend.One ||
							ret.AlphaBlend.DestBlend != Blend.Zero;
						break;

					case "rgbgen":
						switch( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ).ToLowerInvariant() )
						{
						case "wave":
							WaveColorGen waveCgen = new WaveColorGen();
							waveCgen.Waveform = ParseWaveform( reader );
							ret.RgbGen = waveCgen;
							break;

						case "const":
							ConstantColorGen constCgen = new ConstantColorGen();
							constCgen.Color = new Color( ParseVector3( reader ) );
							ret.RgbGen = constCgen;
							break;

						case "identity":
							ConstantColorGen identityCgen = new ConstantColorGen();
							identityCgen.Color = Color.White;
							ret.RgbGen = identityCgen;
							break;

						case "vertex":
						case "exactvertex":
							ret.RgbGen = new VertexColorGen();
							break;

						case "oneminusvertex":
							VertexColorGen vertCgen = new VertexColorGen();
							vertCgen.InvertValue = true;
							ret.RgbGen = vertCgen;
							break;

						default:
							HandleInvalidParam( reader, "rgbgen" );
							break;
						}
						break;

					case "alphagen":
						switch( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ).ToLowerInvariant() )
						{
						case "wave":
							WaveAlphaGen waveAgen = new WaveAlphaGen();
							waveAgen.Waveform = ParseWaveform( reader );
							ret.AlphaGen = waveAgen;
							break;

						case "const":
							float a = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );

							ConstantAlphaGen constAgen = new ConstantAlphaGen();
							constAgen.Alpha = (byte)(Helpers.Saturate( a ) * 0xFF);
							ret.AlphaGen = constAgen;
							break;

						case "identity":
							ConstantAlphaGen identityAgen = new ConstantAlphaGen();
							identityAgen.Alpha = 0xFF;
							ret.AlphaGen = identityAgen;
							break;

						case "vertex":
							ret.AlphaGen = new VertexAlphaGen();
							break;

						case "oneminusvertex":
							VertexAlphaGen vertAgen = new VertexAlphaGen();
							vertAgen.InvertValue = true;
							ret.AlphaGen = vertAgen;
							break;

						default:
							HandleInvalidParam( reader, "alphagen" );
							break;
						}
						break;

					case "texgen":
					case "tcgen":
						switch( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ).ToLowerInvariant() )
						{
						case "base":
						case "texture":
							bundle.TexCoordGen = new TextureTexCoordGen();
							break;

						case "lightmap":
							bundle.TexCoordGen = new LightmapTexCoordGen();
							break;

						case "environment":
							bundle.TexCoordGen = new EnvironmentMappedTexCoordGen();
							break;

						case "vector":
							VectorTexCoordGen vecTcGen = new VectorTexCoordGen();
							vecTcGen.VectorU = ParseVector3( reader );
							vecTcGen.VectorV = ParseVector3( reader );
							bundle.TexCoordGen = vecTcGen;
							break;

						default:
							HandleInvalidParam( reader, "tcgen" );
							break;
						}
						break;

					case "tcmod":
						switch( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ).ToLowerInvariant() )
						{
						case "turb":
							TurbTexCoordMod turbTcMod = new TurbTexCoordMod();
							turbTcMod.Base = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							turbTcMod.Amplitude = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							turbTcMod.Phase = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							turbTcMod.Frequency = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							bundle.TexCoordMods.Add( turbTcMod );
							break;

						case "scale":
							Vector2 scaleVec;
							scaleVec.X = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							scaleVec.Y = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							ScaleTexCoordMod scaleTcMod = new ScaleTexCoordMod();
							scaleTcMod.Scale = scaleVec;
							bundle.TexCoordMods.Add( scaleTcMod );
							break;

						case "scroll":
							Vector2 scrollVec;
							scrollVec.X = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							scrollVec.Y = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							ScrollTexCoordMod scrollTcMod = new ScrollTexCoordMod();
							scrollTcMod.Speed = scrollVec;
							bundle.TexCoordMods.Add( scrollTcMod );
							break;

						case "stretch":
							StretchTexCoordMod stretchTcMod = new StretchTexCoordMod();
							stretchTcMod.Waveform = ParseWaveform( reader );
							bundle.TexCoordMods.Add( stretchTcMod );
							break;

						case "transform":
							TransformTexCoordMod transformTcMod = new TransformTexCoordMod();
							Matrix2x3 transformMat;
							transformMat.M11 = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							transformMat.M21 = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							transformMat.M12 = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							transformMat.M22 = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							transformMat.M13 = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							transformMat.M23 = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							transformTcMod.Matrix = transformMat;
							bundle.TexCoordMods.Add( transformTcMod );
							break;

						case "rotate":
							RotateTexCoordMod rotateTcMod = new RotateTexCoordMod();
							rotateTcMod.Speed = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
							bundle.TexCoordMods.Add( rotateTcMod );
							break;

						default:
							HandleInvalidParam( reader, "tcmod" );
							break;
						}
						break;

					case "depthwrite":
						ret.DepthTest.WriteEnable = true;
						explicitDepth = true;
						break;

					default:
						HandleInvalidParam( reader, "[shader stage parameter]" );
						return null;
					}
				}

				if( bundle.Images.Count > 0 )
				{
					EndBundle( bundle );
					ret.Bundles.Add( bundle );
				}

				return ret;
			}
			catch( Exception ex )
			{
				throw new InvalidContentException( "Error parsing shader stage.", reader.LastTokenIdentity, ex );
			}
		}

		private Vector3 ParseVector3( ContentTokenReader reader )
		{
			reader.ReadExactToken( "(", TokenReadFlags.NoLineBreak );
			
			float x = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
			float y = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
			float z = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );

			reader.ReadExactToken( ")", TokenReadFlags.NoLineBreak );

			return new Vector3( x, y, z );
		}

		private TextureBundle StartBundle()
		{
			TextureBundle ret = new TextureBundle();
			ret.TexCoordGen = null;
			return ret;
		}

		private void EndBundle( TextureBundle bundle )
		{
			if( bundle.Images.Count == 0 )
				throw new InvalidContentException( "Texture bundle with no images." );

			if( bundle.TexCoordGen == null )
			{
				if( bundle.Images[0] == TextureBundle.Lightmap )
					bundle.TexCoordGen = new LightmapTexCoordGen();
				else
					bundle.TexCoordGen = new TextureTexCoordGen();
			}
		}

		private Blend ParseBlend( string blend )
		{
			switch( blend.ToLowerInvariant() )
			{
			case "gl_one":
				return Blend.One;
			
			case "gl_zero":
				return Blend.Zero;
			
			case "gl_src_color":
				return Blend.SourceColor;
			
			case "gl_one_minus_src_color":
				return Blend.InverseSourceColor;
			
			case "gl_src_alpha":
				return Blend.SourceAlpha;
			
			case "gl_one_minus_src_alpha":
				return Blend.InverseSourceAlpha;
			
			case "gl_dst_color":
				return Blend.DestinationColor;
			
			case "gl_one_minus_dst_color":
				return Blend.InverseDestinationColor;
			
			case "gl_dst_alpha":
				return Blend.DestinationAlpha;
			
			case "gl_one_minus_dst_alpha":
				return Blend.InverseDestinationAlpha;

			default:
				throw new InvalidContentException( "Invalid blend constant." );
			}
		}

		private SkyParams ParseSky( ContentTokenReader reader )
		{
			string texture = reader.ReadToken( TokenReadFlags.NoLineBreak );
			reader.ReadToEndOfLine();

			if( texture == null || texture == "-" )
				return null;

			SkyParams ret = new SkyParams();
			ret.Image = texture;
			return ret;
		}

		private Waveform ParseWaveform( ContentTokenReader reader )
		{
			Waveform ret = new Waveform();

			switch( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ).ToLowerInvariant() )
			{
			case "sin":
				ret.Function = WaveFunction.Sine;
				break;

			case "square":
				ret.Function = WaveFunction.Square;
				break;

			case "triangle":
				ret.Function = WaveFunction.Triangle;
				break;

			case "sawtooth":
				ret.Function = WaveFunction.Sawtooth;
				break;

			case "inversesawtooth":
				ret.Function = WaveFunction.InverseSawtooth;
				break;

			case "noise":
				ret.Function = WaveFunction.Noise;
				break;

			default:
				HandleInvalidParam( reader, "waveform" );
				return new Waveform();
			}

			try
			{
				ret.Base = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
				ret.Amplitude = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
				ret.Phase = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
				ret.Frequency = float.Parse( reader.ReadRequiredToken( TokenReadFlags.NoLineBreak ) );
			}
			catch( Exception ex )
			{
				throw new InvalidContentException( "Error parsing waveform.", reader.LastTokenIdentity, ex );
			}

			return ret;
		}

		private void LogParseWarning( ContentIdentity identity, string message, params object[] args )
		{
			if( logger != null )
				logger.LogWarning( string.Empty, identity, message, args );
		}

		private void HandleInvalidParam( ContentTokenReader reader, string prop )
		{
			LogParseWarning( reader.LastTokenIdentity, "Invalid parameter value '{0}' for '{1}'.", reader.LastTokenIdentity, prop );
			reader.ReadToEndOfLine();
		}

	}

}